package com.rstrulak.core.dao;

import java.io.Serializable;
import java.lang.reflect.ParameterizedType;
import java.sql.SQLException;
import java.util.List;
import java.util.Map;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.hibernate.Criteria;
import org.hibernate.HibernateException;
import org.hibernate.Query;
import org.hibernate.Session;
import org.hibernate.criterion.DetachedCriteria;
import org.hibernate.criterion.Order;
import org.hibernate.criterion.Projection;
import org.hibernate.criterion.Projections;
import org.hibernate.impl.CriteriaImpl;
import org.hibernate.transform.ResultTransformer;
import org.springframework.orm.hibernate3.HibernateCallback;
import org.springframework.orm.hibernate3.support.HibernateDaoSupport;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import com.rstrulak.constant.GenericFilter;
import com.rstrulak.constant.PageInfo;
import com.rstrulak.constant.PresentationInfo;
import com.rstrulak.constant.SortField;
import com.rstrulak.constant.SortInfo;

@Transactional(propagation = Propagation.REQUIRED, readOnly = false)
public class GenericDaoImpl<T, PK extends Serializable> extends HibernateDaoSupport implements GenericDao<T, PK> {
    /**
     * Log variable for all child classes.
     */
    protected final Log log = LogFactory.getLog(getClass());

    /**
     * Persistent type class.
     */
    private Class<T> persistentClass;

    /**
     * Constructor that takes in a class to see which type of entity to persist.
     * 
     * @param persistentClass the class type to persist
     */
    public GenericDaoImpl(final Class<T> persistentClass) {
        this.persistentClass = persistentClass;
    }

    /**
     * Constructor for classes extending this one. It determines the type of
     * entity to persist based on the subclass declaration.
     */
    @SuppressWarnings("unchecked")
    protected GenericDaoImpl() {
        this.persistentClass = (Class<T>) ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0];
    }

    /**
     * {@inheritDoc}
     */
    @SuppressWarnings("unchecked")
    public PK save(T o) {
        // TODO: Thing about merge
        return (PK) getHibernateTemplate().save(o);
    }

    /**
     * {@inheritDoc}
     */
    public void update(T o) {
        getHibernateTemplate().update(o);
    }

    /**
     * {@inheritDoc}
     */
    public void saveOrUpdate(T o) {
        getHibernateTemplate().saveOrUpdate(o);
    }

    /**
     * {@inheritDoc}
     */
    public void delete(PK id) {
        delete(this.getById(id));
    }

    /**
     * {@inheritDoc}
     */
    public void delete(T o) {
        getHibernateTemplate().delete(o);
    }

    /**
     * {@inheritDoc}
     */
    public T getById(PK id) {
        return getHibernateTemplate().get(this.persistentClass, id);
    }

    /**
     * {@inheritDoc} This implementation checks the existence of an object by
     * simply retrieving it.
     */
    public boolean exists(PK id) {
        T entity = getHibernateTemplate().get(this.persistentClass, id);
        return entity != null;
    }

    /**
     * {@inheritDoc}
     */
    public List<T> getAll() {
        return getHibernateTemplate().loadAll(this.persistentClass);
    }

    /**
     * {@inheritDoc}
     */
    public List<T> getAll(final PageInfo pageInfo) {
        return (List<T>) getHibernateTemplate().execute(new HibernateCallback() {
            public Object doInHibernate(Session session) throws HibernateException, SQLException {
                Criteria criteria = createCriteria(session);
                applyPagingToCriteria(criteria, pageInfo);
                return criteria.list();
            }
        });
    }

    /**
     * {@inheritDoc}
     */
    public List<T> getAll(final SortInfo sortInfo) {
        return (List<T>) getHibernateTemplate().execute(new HibernateCallback() {
            public Object doInHibernate(Session session) throws HibernateException, SQLException {
                Criteria criteria = createCriteria(session);
                applySortingToCriteria(criteria, sortInfo);
                return criteria.list();
            }
        });
    }

    /**
     * {@inheritDoc}
     */
    public List<T> getAll(final PageInfo pageInfo, final SortInfo sortInfo) {
        
        return (List<T>) getHibernateTemplate().execute(new HibernateCallback() {
            public Object doInHibernate(Session session) throws HibernateException, SQLException {
                Criteria criteria = createCriteria(session);
                applySortingToCriteria(applyPagingToCriteria(criteria, pageInfo), sortInfo);
                return criteria.list();
            }
        });
    }

    /**
     * {@inheritDoc}
     */
    public List<T> getAll(final PresentationInfo presentationInfo) {
        return (List<T>) getHibernateTemplate().execute(new HibernateCallback() {
            public Object doInHibernate(Session session) throws HibernateException, SQLException {
                Criteria criteria = createCriteria(session);
                if (presentationInfo.isPagingIncluded()) {
                    criteria = applyPagingToCriteria(criteria, presentationInfo.getPageInfo());
                }
                if (presentationInfo.isSortingIncluded()) {
                    criteria = applySortingToCriteria(criteria, presentationInfo.getSortInfo());
                }
                return criteria.list();
            }
        });
    }

    /**
     * {@inheritDoc}
     */
    public List<T> getAll(final GenericFilter filter, final PresentationInfo presentationInfo) {
        return (List<T>) getHibernateTemplate().execute(new HibernateCallback() {
            public Object doInHibernate(Session session) throws HibernateException, SQLException {
                Criteria criteria = createCriteria(session);
                if (presentationInfo.isPagingIncluded()) {
                    criteria = applyPagingToCriteria(criteria, presentationInfo.getPageInfo());
                }
                if (presentationInfo.isSortingIncluded()) {
                    criteria = applySortingToCriteria(criteria, presentationInfo.getSortInfo());
                }
                if (filter != null) {
                    filter.addRestrictionFromFilterFields(criteria);
                }
                return criteria.list();
            }
        });
    }

    /**
     * {@inheritDoc}
     */
    @SuppressWarnings("unchecked")
    public List<T> findByQuery(String query) {
        return getHibernateTemplate().find(query);
    }

    /**
     * {@inheritDoc}
     */
    @SuppressWarnings("unchecked")
    public List<T> findByNamedQuery(String queryName, Map<String, Object> queryParams) {
        String[] params = new String[queryParams.size()];
        Object[] values = new Object[queryParams.size()];

        int index = 0;
        for (String s : queryParams.keySet()) {
            params[index] = s;
            values[index++] = queryParams.get(s);
        }

        return getHibernateTemplate().findByNamedQueryAndNamedParam(queryName, params, values);
    }

    /**
     * Creates and returns Criteria for the <tt>persistentClass</tt>.
     * 
     * @return Hibernate Criteria instance
     */
    protected Criteria createCriteria(Session session) {
        return session.createCriteria(this.persistentClass);
    }
    
    protected DetachedCriteria createDetachedCriteria() {
        return DetachedCriteria.forClass(this.persistentClass);
    }

    /**
     * Retrieves a single object based on a <tt>criteria</tt> passed as an
     * argument.
     * 
     * @param criteria Hibernate Criteria instance
     * @return Single object or <tt>null</tt> if none found
     * @throws HibernateException - if there is more than one matching result
     */
    @SuppressWarnings("unchecked")
    protected T getObjectByCriteria(final DetachedCriteria criteria) {
        // return (T)criteria.uniqueResult();
        // we execute criteria through hibernate template to get consistent
        // spring only exceptions stack
        return (T) getHibernateTemplate().execute(new HibernateCallback() {
            public Object doInHibernate(Session session) throws HibernateException, SQLException {
                return criteria.getExecutableCriteria(session).uniqueResult();
            }
        });
    }

    /**
     * Retrieves a list of objects based on a <tt>criteria</tt> passed as an
     * argument.
     * 
     * @param criteria Hibernate Criteria instance
     * @return List of object matching the criteria
     */
    @SuppressWarnings("unchecked")
    protected List<T> getListByCriteria(final DetachedCriteria criteria) {
        // return (List<T>)criteria.list();
        // we execute criteria through hibernate template to get consistent
        // spring only exceptions stack
        return (List<T>) getHibernateTemplate().execute(new HibernateCallback() {
            public Object doInHibernate(Session session) throws HibernateException, SQLException {
                return criteria.getExecutableCriteria(session).list();
            }
        });
    }

    /**
     * Applies paging contained in <tt>pageInfo</tt> to the supplied
     * <tt>criteria</tt>. Executes a query to count total number of rows that
     * will be returned by the criteria and sets this value in the
     * <tt>pageInfo</tt> parameter.
     * 
     * @param criteria - criteria to apply sorting to, IN parameter
     * @param pageInfo - paging informations to apply, IN/OUT parameter
     * @return the paging criteria
     */
    protected Criteria applyPagingToCriteria(final Criteria criteria, final PageInfo pageInfo) {
        // method 1
        CriteriaImpl criteriaImpl;
        try {
            criteriaImpl = (CriteriaImpl) criteria;
        } catch (ClassCastException e) {
            log.error("Could not cast Criteria to CriteriaImpl - perhaps Hibernate implementation has changed", e);
            throw new UnsupportedOperationException("Requested operation could not be compeleted - probably due to some changes in Hibernate implementation.",
                    e);
        }
        Projection originalProjection = criteriaImpl.getProjection();
        ResultTransformer originalResultTransformer = criteriaImpl.getResultTransformer();

        criteria.setProjection(Projections.rowCount());
        Integer resultsCount = (Integer) criteria.uniqueResult();

        criteria.setProjection(originalProjection);
        criteria.setResultTransformer(originalResultTransformer);

        criteria.setMaxResults(pageInfo.getPageSize());
        criteria.setFirstResult(pageInfo.getPageSize() * pageInfo.getPageNo());

        pageInfo.setResultsCount(resultsCount);
        pageInfo.setPagesCount((int) Math.ceil((double) resultsCount / pageInfo.getPageSize()));

        // method 2
        // assign null to projection assuming we do not handle projections

        // method 3
        // Criteria clonedCriteria =
        // (Criteria)SerializationHelper.clone((CriteriaImpl)criteria);

        // method 4
        // Create custom MutableDetachedCriteria
        return criteria;
    }
    
    
    protected Criteria applyPagingToQuery(final Query query, final T domainObject, final PageInfo pageInfo) {
        Criteria criteria = (CriteriaImpl) getSession().createCriteria(domainObject.getClass());
        
        criteria.setProjection(Projections.rowCount());
        Integer resultsCount = (Integer) criteria.uniqueResult();


        query.setMaxResults(pageInfo.getPageSize());
        query.setFirstResult(pageInfo.getPageSize() * pageInfo.getPageNo());

        pageInfo.setResultsCount(resultsCount);
        pageInfo.setPagesCount((int) Math.ceil((double) resultsCount / pageInfo.getPageSize()));
        return criteria;
    }

    /**
     * Applies sorting contained in <tt>sortInfo</tt> to the supplied
     * <tt>criteria</tt>.
     * 
     * @param criteria - criteria to apply sorting to, IN parameter
     * @param sortInfo - sorting informations to apply, IN parameter
     * @return the sorting criteria
     */
    protected Criteria applySortingToCriteria(final Criteria criteria, final SortInfo sortInfo) {
        for (SortField sortField : sortInfo.getFields()) {
            if (sortField.isOrderAscending()) {
                criteria.addOrder(Order.asc(sortField.getName()));
            } else {
                criteria.addOrder(Order.desc(sortField.getName()));
            }
        }
        return criteria;
    }
    
    /**
     * {@inheritDoc}
     */
    @Override
    public int getResultCount(final GenericFilter filter) {
        return (Integer) getHibernateTemplate().execute(new HibernateCallback() {
            public Object doInHibernate(Session session) throws HibernateException, SQLException {
                Criteria criteria = createCriteria(session);
                if (filter != null) {
                    filter.addRestrictionFromFilterFields(criteria);
                }
                criteria.setProjection(Projections.rowCount());
                return (Integer) criteria.uniqueResult();
            }
        });
    }

}
